OpenGL 座標在經過 Shader 轉換之後,所有的座標都會變標準化設備座標 Normalized Device Coordinates (NDC),也就是所有的x
y
z
都在$[-1.0, 1.0]$之間。超出此範圍的座標就不會顯示。
座標在被轉換成螢幕座標(Screen-Space)時還會經過多次轉換
為了將坐標從一個坐標系變換到另一個坐標系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣
glViewport
所定義的座標範圍內之所以把頂點在不同坐標系中轉換,是因為有些操作在特定的坐標系中才有意義且方便。例如,需要對物體修改時,在 Local Space 比較方便;如果需要相對其他物體時,在 World Space 比較方便。
局部空間是指物件所在的坐標空間,一個物體的原點$(0,0,0)$
但最後可能出現在世界的不同地方(座標),它的所有頂點都是相對於 Local Space 的原點,這些座標都是局部(Local)的。
頂點相對於遊戲世界原點的座標。從 Local Space 變換到 World Space 是由 Model Matrix 來完成的
把物體從 Local Space 經過位移、縮放、旋轉來把物體擺在世界的位置
觀察空間是將世界座標轉成使用者視野(攝影機 Camera)前方的座標。經由位移和旋轉場景,使得特定的物體變換到攝影機(Camera)的前方,此種變換的矩陣稱作 View Matrix
在經過 Vertex Shader 後 OpenGL 希望所有的座標都在 $[-1.0, 1.0]$ 內,超出的會被裁剪(Clip) 掉(忽略掉),在此座標內的頂點最後會被光柵化成 Fragment
為了將頂點座標變換到 NDC 空間,要經由 Projection Matrix 投影矩陣,它指定了一個範圍的座標 e.g. [-1000, 1000],投影矩陣會將在範圍內的座標轉成 NDC。超出範圍的座標,轉出的座標會超出 -1.0 或 1.0,最後會被剪裁掉。
因此會有個範圍內的頂點都會被轉換,這個範圍叫做 觀察箱Viewing Box又稱作 Frustum
這個轉換的過程稱作 投影 Projection
有兩種不同的投影方式:正射投影、透視投影
正射投影定義了一個像長方體的觀察箱,超出這個觀察箱外的頂點會被剪裁掉,在此觀察箱內的頂點會被轉成 NDC 座標。
由寬、高及近(Near)平面和遠(Far)平面定義
// glm::ortho(left, right, bottom, top, zNear, zFar)
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
:::spoiler Details
出來的矩陣是:
$\begin{pmatrix}
\frac{2}{\text{right}-\text{left}} & 0 & 0 & t_x \
0 & \frac{2}{\text{top} - \text{bottom}} & 0 & t_y \
0 & 0 & \frac{2}{\text{zFar} - \text{zNear}} & t_z \
0 & 0 & 0 & 1
\end{pmatrix}$
$t_x = - \frac{\text{right}+\text{left}}{\text{right}-\text{left}} \
t_y = - \frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} \
t_z = - \frac{\text{zFar} + \text{zNear}}{\text{zFar} - \text{zNear}}$
TODO 理解
:::
現實中,離你越遠的東西看起來更小。這個奇怪的效果稱之為透視(Perspective)。透視的效果在我們看一條無限長的高速公路或鐵路時尤其明顯,正如下面圖片顯示的那樣:
正如你看到的那樣,由於透視,這兩條線在很遠的地方看起來會相交。這正是透視投影想要模仿的效果,它是使用透視投影矩陣來完成的。這個投影矩陣將給定的平截頭體範圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大。被變換到裁剪空間的坐標都會在-w到w的範圍之間(任何大於這個範圍的坐標都會被裁剪掉)。
glm::mat4 proj = glm::perspective(
glm::radians(45.0f), // Field Of View
(float)width/(float)height // 寬高比
, 0.1f, 100.0f); // 近平面, 遠平面
glm::perspective(fovy, aspect, near, far)
fovy
Field of View 視野大小aspect
寬高比 $\frac{\text{width}}{\text{height}}$near
近平面 (距離)far
遠平面 (距離)正射投影(Orthographic)不會產生透視(w分量是1.0)所以看起來遠處的物體跟近處是一樣的大小,主要用在渲染 2D 或是建築、工程、或建模的應用。
透視投影(Perspective)遠處的的物體就看起來較小。
一個頂點座標會經過下列算式轉成 Clip Space。注意矩陣運算式從右往左。
隨然我們是要做2D的東西,但我們還是來嘗試一下3D的物品
在開始進行3D繪圖時,我們首先創建一個模型矩陣。這個模型矩陣包含了位移、縮放與旋轉操作,它們會被應用到所有物體的頂點上,以變換它們到全局的世界空間。讓我們變換一下我們的平面,將其繞著x軸旋轉,使它看起來像放在地上一樣。這個模型矩陣看起來是這樣的:
glm::mat4 model(1.f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec(1.0f, 0.0f, 0.0f));
// 繞 x 軸旋轉 -55 度
OpenGL 的座標是右手坐標系
我們先假設我們的 Camera,也就是看出去的座標是固定不動的。我們希望看到 Camera 放在 ${0, 0, 3}$看出去的效果,我們就將整個物體往反方向(0, 0, -3)移動就好了
glm::mat4 view(1.f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
最後我們希望的透視效果
glm::mat4 projection(1.0f);
projection = glm::perspective(glm::radians(45.0f), width / height, 0.1f, 100.f);
接著傳入 Shader 中
#version 330 core
layout (location = 0) in vec3 aPos;
// ...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
// 注意矩陣乘法
gl_Position = projection * view * model * vec4(aPos, 1.0);
...
}
最終的結果:
要畫一個3D立方體要36個點,所以改用glDrawArrays
。
並且我們讓他隨著時間旋轉。
這的確有點像是一個立方體,但又有種說不出的奇怪。立方體的某些本應被遮擋住的面被繪製在了這個立方體其他面之上。之所以這樣是因為OpenGL是一個三角形一個三角形地來繪製你的立方體的,所以即便之前那裡有東西它也會覆蓋之前的像素。因為這個原因,有些三角形會被繪製在其它三角形上面,雖然它們本不應該是被覆蓋的。
幸運的是,OpenGL存儲深度信息在一個叫做Z緩衝(Z-buffer)的緩衝中,它允許OpenGL決定何時覆蓋一個像素而何時不覆蓋。通過使用Z緩衝,我們可以配置OpenGL來進行深度測試。
OpenGL 的 Z-Buffer (Z 緩衝)讓 OpenGL 知道一個像素的深度 (Depth)(可以想像成圖層那樣),進行深度測試(Depth Test)。
啟用 Depth Test
glEnable(GL_DEPTH_TEST);
每一 frame 都要清掉 Z-Buffer 的內容
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
這樣看起來就這正常了吧
程式碼在這裡
如果想要在螢幕上畫出10個立方體,首先我們要定義每個立方體的位移量。
std::vector<glm::vec3> cubePositions = {
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3(2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3(1.3f, -2.0f, -2.5f),
glm::vec3(1.5f, 2.0f, -2.5f),
glm::vec3(1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
接在遊戲循環的時候,調用glDrawArrays
10次
for(int i = 0; i < cubePositions.size(); i++)
{
float angle = 20.0f * i;
model = glm::translate(model, glm::vec3(cubePositions[i]));
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 3.0f, 5.0f));
UploadMat4("vModel", model);
model = glm::mat4(1.f);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
就會長這樣
https://learnopengl.com/Getting-started/Coordinate-Systems